// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package org.jetbrains.kotlin.idea.quickfix.migration

import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.CleanupFix
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
import org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention
import org.jetbrains.kotlin.idea.intentions.declarations.ConvertMemberToExtensionIntention
import org.jetbrains.kotlin.idea.project.builtIns
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.js.PredefinedAnnotation
import org.jetbrains.kotlin.js.resolve.diagnostics.ErrorsJs
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode

class MigrateExternalExtensionFix(declaration: KtNamedDeclaration) : KotlinQuickFixAction<KtNamedDeclaration>(declaration), CleanupFix {

    override fun getText() = KotlinBundle.message("fix.with.asdynamic")
    override fun getFamilyName() = text

    override fun startInWriteAction(): Boolean = false

    override fun invoke(project: Project, editor: Editor?, file: KtFile) {
        val declaration = element ?: return

        when {
            isMemberExtensionDeclaration(declaration) -> fixExtensionMemberDeclaration(declaration, editor)
            isMemberDeclaration(declaration) -> declaration.containingClassOrObject?.let(::fixNativeClass)
            declaration is KtClassOrObject -> fixNativeClass(declaration)
        }
    }

    private fun fixNativeClass(containingClass: KtClassOrObject) {
        val membersToFix =
            containingClass.declarations.asSequence().filterIsInstance<KtCallableDeclaration>()
                .filter { isMemberDeclaration(it) && !isMemberExtensionDeclaration(it) }.map {
                    it to fetchJsNativeAnnotations(it)
                }.filter {
                    it.second.annotations.isNotEmpty()
                }.toList()

        membersToFix.asReversed().forEach { (memberDeclaration, annotations) ->
            if (annotations.nativeAnnotation != null && !annotations.isGetter && !annotations.isSetter && !annotations.isInvoke) {
                convertNativeAnnotationToJsName(memberDeclaration, annotations)
                annotations.nativeAnnotation.delete()
            } else {
                val externalDeclaration = ConvertMemberToExtensionIntention.Holder.convert(memberDeclaration)
                runWriteAction {
                    fixExtensionMemberDeclaration(externalDeclaration, null) // editor is null as we are not going to open any live templates
                }
            }
        }

        // make class external
        val classAnnotations = fetchJsNativeAnnotations(containingClass)
        fixAnnotations(containingClass, classAnnotations, null)
    }

    private data class JsNativeAnnotations(
        val annotations: List<KtAnnotationEntry>,
        val nativeAnnotation: KtAnnotationEntry?,
        val isGetter: Boolean,
        val isSetter: Boolean,
        val isInvoke: Boolean
    )

    private fun fetchJsNativeAnnotations(declaration: KtNamedDeclaration): JsNativeAnnotations {
        var isGetter = false
        var isSetter = false
        var isInvoke = false
        var nativeAnnotation: KtAnnotationEntry? = null
        val nativeAnnotations = ArrayList<KtAnnotationEntry>()

        declaration.modifierList?.annotationEntries?.forEach {
            when {
                it.isJsAnnotation(PredefinedAnnotation.NATIVE_GETTER) -> {
                    isGetter = true
                    nativeAnnotations.add(it)
                }
                it.isJsAnnotation(PredefinedAnnotation.NATIVE_SETTER) -> {
                    isSetter = true
                    nativeAnnotations.add(it)
                }
                it.isJsAnnotation(PredefinedAnnotation.NATIVE_INVOKE) -> {
                    isInvoke = true
                    nativeAnnotations.add(it)
                }
                it.isJsAnnotation(PredefinedAnnotation.NATIVE) -> {
                    nativeAnnotations.add(it)
                    nativeAnnotation = it
                }
            }
        }
        return JsNativeAnnotations(nativeAnnotations, nativeAnnotation, isGetter, isSetter, isInvoke)
    }

    private fun fixExtensionMemberDeclaration(declaration: KtNamedDeclaration, editor: Editor?) {
        val name = declaration.nameAsSafeName
        val annotations = fetchJsNativeAnnotations(declaration)
        runWriteAction {
            fixAnnotations(declaration, annotations, editor)

            val psiFactory = KtPsiFactory(declaration.project)
            val body = psiFactory.buildExpression {
                appendName(Name.identifier("asDynamic"))
                when {
                    annotations.isGetter -> {
                        appendFixedText("()")
                        if (declaration is KtNamedFunction) {
                            appendParameters(declaration, "[", "]")
                        }
                    }

                    annotations.isSetter -> {
                        appendFixedText("()")
                        if (declaration is KtNamedFunction) {
                            appendParameters(declaration, "[", "]", skipLast = true)
                            declaration.valueParameters.last().nameAsName?.let {
                                appendFixedText(" = ")
                                appendName(it)
                            }
                        }
                    }

                    annotations.isInvoke -> {
                        appendFixedText("()")
                        if (declaration is KtNamedFunction) {
                            appendParameters(declaration, "(", ")")
                        }
                    }

                    else -> {
                        appendFixedText("().")
                        appendName(name)
                        if (declaration is KtNamedFunction) {
                            appendParameters(declaration, "(", ")")
                        }
                    }
                }
            }

            if (declaration is KtNamedFunction) {
                declaration.bodyExpression?.delete()
                declaration.equalsToken?.delete()

                if (annotations.isSetter || annotations.isInvoke) {
                    val blockBody = psiFactory.createSingleStatementBlock(body)
                    declaration.add(blockBody)
                } else {
                    declaration.add(psiFactory.createEQ())
                    declaration.add(body)
                }
            } else if (declaration is KtProperty) {
                declaration.setter?.delete()
                declaration.getter?.delete()
                val getter = psiFactory.createPropertyGetter(body)
                declaration.add(getter)

                if (declaration.isVar) {
                    val setterBody = psiFactory.buildExpression {
                        appendName(Name.identifier("asDynamic"))
                        appendFixedText("().")
                        appendName(name)
                        appendFixedText(" = ")
                        appendName(Name.identifier("value"))
                    }

                    val setterStubProperty = psiFactory.createProperty("val x: Unit set(value) { Unit }")
                    val block = setterStubProperty.setter!!.bodyBlockExpression!!
                    block.statements.single().replace(setterBody)
                    declaration.add(setterStubProperty.setter!!)
                }
            }

            Unit
        }
    }

    private fun fixAnnotations(declaration: KtNamedDeclaration, annotations: JsNativeAnnotations, editor: Editor?) {
        annotations.annotations.forEach { it.delete() }

        if (declaration is KtClassOrObject) {
            declaration.addModifier(KtTokens.EXTERNAL_KEYWORD)
        } else {
            declaration.addModifier(KtTokens.INLINE_KEYWORD)
            declaration.removeModifier(KtTokens.EXTERNAL_KEYWORD)
        }

        if (declaration is KtFunction) {
            declaration.addAnnotation(StandardNames.FqNames.suppress, "\"NOTHING_TO_INLINE\"")
        }

        convertNativeAnnotationToJsName(declaration, annotations)

        if (declaration is KtFunction && !declaration.hasDeclaredReturnType() && !annotations.isSetter && !annotations.isInvoke && editor != null) {
            SpecifyTypeExplicitlyIntention.addTypeAnnotation(editor, declaration, declaration.builtIns.unitType)
        }
    }

    private fun convertNativeAnnotationToJsName(declaration: KtNamedDeclaration, annotations: JsNativeAnnotations) {
        val nativeAnnotation = annotations.nativeAnnotation
        if (nativeAnnotation != null && nativeAnnotation.valueArguments.isNotEmpty()) {
            declaration.addAnnotation(FqName("JsName"), nativeAnnotation.valueArguments.joinToString { it.asElement().text })
        }
    }

    private fun BuilderByPattern<KtExpression>.appendParameters(
        declaration: KtNamedFunction,
        lParenth: String,
        rParenth: String,
        skipLast: Boolean = false
    ) {
        appendFixedText(lParenth)
        for ((index, param) in declaration.valueParameters.let { if (skipLast) it.take(it.size - 1) else it }.withIndex()) {
            param.nameAsName?.let { paramName ->
                if (index > 0) {
                    appendFixedText(",")
                }
                appendName(paramName)
            }
        }
        appendFixedText(rParenth)
    }

    companion object : KotlinSingleIntentionActionFactory() {
        private fun KtAnnotationEntry.isJsAnnotation(vararg predefinedAnnotations: PredefinedAnnotation): Boolean {
            val bindingContext = analyze(BodyResolveMode.PARTIAL)
            val annotationDescriptor = bindingContext[BindingContext.ANNOTATION, this]
            return annotationDescriptor != null && predefinedAnnotations.any { annotationDescriptor.fqName == it.fqName }
        }

        private fun KtAnnotationEntry.isJsNativeAnnotation(): Boolean = isJsAnnotation(
            PredefinedAnnotation.NATIVE,
            PredefinedAnnotation.NATIVE_GETTER,
            PredefinedAnnotation.NATIVE_SETTER,
            PredefinedAnnotation.NATIVE_INVOKE
        )

        private fun isMemberExtensionDeclaration(psiElement: PsiElement): Boolean {
            return (psiElement is KtNamedFunction && psiElement.receiverTypeReference != null) ||
                    (psiElement is KtProperty && psiElement.receiverTypeReference != null)
        }

        private fun isMemberDeclaration(psiElement: PsiElement): Boolean =
            (psiElement is KtNamedFunction && psiElement.receiverTypeReference == null) || (psiElement is KtProperty && psiElement.receiverTypeReference == null)

        override fun createAction(diagnostic: Diagnostic): IntentionAction? {
            val e = diagnostic.psiElement
            when (diagnostic.factory) {
                ErrorsJs.WRONG_EXTERNAL_DECLARATION -> {
                    if (isMemberExtensionDeclaration(e) && e.getParentOfType<KtClassOrObject>(true) == null) {
                        return MigrateExternalExtensionFix(e as KtNamedDeclaration)
                    }
                }
                Errors.DEPRECATION_ERROR, Errors.DEPRECATION -> {
                    if (e.getParentOfType<KtAnnotationEntry>(false)?.isJsNativeAnnotation() == true) {
                        e.getParentOfType<KtNamedDeclaration>(false)?.let {
                            return MigrateExternalExtensionFix(it)
                        }
                    }
                    if ((e as? KtNamedDeclaration)?.modifierList?.annotationEntries?.any { it.isJsNativeAnnotation() } == true) {
                        return MigrateExternalExtensionFix(e)
                    }
                }
            }

            return null
        }
    }
}
